/*******************************************************************************
 * This file is part of Crunch Graphics.
 *
 * Crunch Graphics is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Graphics is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Graphics.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.graphics.gpu;

import javax.media.opengl.GL;
import javax.media.opengl.GL3;

/** Encapsulates the state of the rasterizer on the GPU.
 */
public class GpuRasterizerState extends GpuObject {
	/** Sets the state of the GPU to the state represented by this object.
	 */
	public void bind() {
		getGpuState().bind(this);
	}

	/** Sets the point size.
	 */
	public void setPointSize(float size) {
		pointSize = size;
	}

	/** Sets the line width.
	 */
	public void setLineWidth(float width) {
		lineWidth = width;
	}

	/** Sets the polygon mode.
	 */
	public void setPolygonMode(int mode) {
		polygonModeFront = mode;
		polygonModeBack = mode;
	}

	/** Sets the polygon modes for front and back faces.
	 */
	public void setPolygonModes(int modeFront, int modeBack) {
		polygonModeFront = modeFront;
		polygonModeBack = modeBack;
	}

	/** Sets whether culling is enabled.
	 */
	public void setCullingEnabled(boolean e) {
		cullingEnabled = e;
	}

	/** Sets which faces should be culled.
	 */
	public void setCullMode(int mode) {
		cullMode = mode;
	}

	/** Sets the orientation of front facing polygons.
	 */
	public void setFrontFaceOrientation(int orientation) {
		frontFaceOrientation = orientation;
	}

	/** Sets whether polygon offset is enabled for polygons drawn in fill mode.
	 */
	public void setPolygonOffsetFillEnabled(boolean e) {
		polygonOffsetFillEnabled = e;
	}

	/** Sets whether polygon offset is enabled for polygons drawn in line mode.
	 */
	public void setPolygonOffsetLineEnabled(boolean e) {
		polygonOffsetLineEnabled = e;
	}

	/** Sets whether polygon offset is enabled for polygons drawn in point mode.
	 */
	public void setPolygonOffsetPointEnabled(boolean e) {
		polygonOffsetPointEnabled = e;
	}

	/** Sets the polygon offset parameters.
	 */
	public void setPolygonOffset(float factor, float units) {
		polygonOffsetFactor = factor;
		polygonOffsetUnits = units;
	}

	/** Returns the point size.
	 */
	public float getPointSize() {
		return pointSize;
	}

	/** Returns the line width.
	 */
	public float getLineWidth() {
		return lineWidth;
	}

	/** Returns the polygon mode for front faces.
	 */
	public int getPolygonModeFront() {
		return polygonModeFront;
	}

	/** Returns the polygon mode for back faces.
	 */
	public int getPolygonModeBack() {
		return polygonModeBack;
	}

	/** Returns whether culling is enabled.
	 */
	public boolean isCullingEnabled() {
		return cullingEnabled;
	}

	/** Returns which faces should be culled.
	 */
	public int getCullMode() {
		return cullMode;
	}

	/** Returns the orientation of front facing polygons.
	 */
	public int getFrontFaceOrientation() {
		return frontFaceOrientation;
	}

	/** Returns whether polygon offset is enabled for polygons drawn in fill mode.
	 */
	public boolean isPolygonOffsetFillEnabled() {
		return polygonOffsetFillEnabled;
	}

	/** Returns whethes polygon offset is enabled for polygons drawn in line mode.
	 */
	public boolean isPolygonOffsetLineEnabled() {
		return polygonOffsetLineEnabled;
	}

	/** Returns whether polygon offset is enabled for polygons drawn in point mode.
	 */
	public boolean isPolygonOffsetPointEnabled() {
		return polygonOffsetPointEnabled;
	}

	/** Returns the polygon offset factor parameter.
	 */
	public float getPolygonOffsetFactor() {
		return polygonOffsetFactor;
	}

	/** Returns the polygon offset units parameter.
	 */
	public float getPolygonOffsetUnits() {
		return polygonOffsetUnits;
	}

	GpuRasterizerState(GpuState state) {
		super(state);
		pointSize = 1.0f;
		lineWidth = 1.0f;
		polygonModeFront = GL3.GL_FILL;
		polygonModeBack = GL3.GL_FILL;

		cullingEnabled = false;
		cullMode = GL.GL_BACK;
		frontFaceOrientation = GL.GL_CCW;

		polygonOffsetFillEnabled = false;
		polygonOffsetLineEnabled = false;
		polygonOffsetPointEnabled = false;
		polygonOffsetFactor = 0.0f;
		polygonOffsetUnits = 0.0f;
	}

	/** Copy constructor.
	 */
	public GpuRasterizerState(GpuRasterizerState other) {
		super(other.getGpuState());
		pointSize = other.pointSize;
		lineWidth = other.lineWidth;
		polygonModeFront = other.polygonModeFront;
		polygonModeBack = other.polygonModeBack;

		cullingEnabled = other.cullingEnabled;
		cullMode = other.cullMode;
		frontFaceOrientation = other.frontFaceOrientation;

		polygonOffsetFillEnabled = other.polygonOffsetFillEnabled;
		polygonOffsetLineEnabled = other.polygonOffsetLineEnabled;
		polygonOffsetPointEnabled = other.polygonOffsetPointEnabled;
		polygonOffsetFactor = other.polygonOffsetFactor;
		polygonOffsetUnits = other.polygonOffsetUnits;
	}

	// NOTE: it appears that polygon mode is only implemented to work with GL_FRONT_AND_BACK
	// attempting to use it with GL_FRONT or GL_BACK separately results in GL_INVALID_ENUM
	// additionally, glGet with GL_POLYGON_MODE results in GL_INVALID_ENUM
	// workaround: use GL_FRONT_AND_BACK only

	private float pointSize;        // the size of points
	private float lineWidth;        // the size of lines
	private int polygonModeFront;   // the polygon mode for front faces
	private int polygonModeBack;    // the polygon mode for back faces

	private boolean cullingEnabled;     // whether polygon culling is enabled
	private int cullMode;               // the mode in which polygons are culled
	private int frontFaceOrientation;   // the orientation of front-facing polygons

	private boolean polygonOffsetFillEnabled;   // whether polygon offset is enabled for polygons drawn in fill mode
	private boolean polygonOffsetLineEnabled;   // whether polygon offset is enabled for polygons drawn in line mode
	private boolean polygonOffsetPointEnabled;  // whether polygon offset is enabled for polygons drawn in point mode
	private float polygonOffsetFactor;          // the factor parameter for polygon offset
	private float polygonOffsetUnits;           // the units parameter for polygon offset

	// compares changes against current state and apply differences
	void apply(GpuRasterizerState currentState) {
		if (pointSize != currentState.pointSize) {
			getGpuState().getGl().glPointSize(pointSize);
			currentState.pointSize = pointSize;
		}

		if (lineWidth != currentState.lineWidth) {
			getGpuState().getGl().glLineWidth(lineWidth);
			currentState.lineWidth = lineWidth;
		}

		// BUG: polygon mode only works with GL_FRONT_AND_BACK

		if (polygonModeFront != currentState.polygonModeFront) {
//		getGpuState().getGl().glPolygonMode(GL.GL_FRONT, polygonModeFront);
			getGpuState().getGl().glPolygonMode(GL.GL_FRONT_AND_BACK, polygonModeFront);
			currentState.polygonModeFront = polygonModeFront;
		}

//	if (polygonModeBack != currentState.polygonModeBack) {
//		getGpuState().getGl().glPolygonMode(GL.GL_BACK, polygonModeBack);
//		currentState.polygonModeBack = polygonModeBack;
//	}

		currentState.cullingEnabled = setStateEnabled(cullingEnabled, currentState.cullingEnabled, GL.GL_CULL_FACE);

		if (cullingEnabled) {
			if (cullMode != currentState.cullMode) {
				getGpuState().getGl().glCullFace(cullMode);
				currentState.cullMode = cullMode;
			}
		}

		// front faces are used in other things than just culling, so set it even if culling isn't enabled
		if (frontFaceOrientation != currentState.frontFaceOrientation) {
			getGpuState().getGl().glFrontFace(frontFaceOrientation);
			currentState.frontFaceOrientation = frontFaceOrientation;
		}

		currentState.polygonOffsetFillEnabled = setStateEnabled(polygonOffsetFillEnabled, currentState.polygonOffsetFillEnabled, GL.GL_POLYGON_OFFSET_FILL);
		currentState.polygonOffsetLineEnabled = setStateEnabled(polygonOffsetLineEnabled, currentState.polygonOffsetLineEnabled, GL3.GL_POLYGON_OFFSET_LINE);
		currentState.polygonOffsetPointEnabled = setStateEnabled(polygonOffsetPointEnabled, currentState.polygonOffsetPointEnabled, GL3.GL_POLYGON_OFFSET_POINT);

		if (polygonOffsetFillEnabled || polygonOffsetLineEnabled || polygonOffsetPointEnabled) {
			if (polygonOffsetFactor != currentState.polygonOffsetFactor ||
					polygonOffsetUnits != currentState.polygonOffsetUnits) {
				getGpuState().getGl().glPolygonOffset(polygonOffsetFactor, polygonOffsetUnits);
				currentState.polygonOffsetFactor = polygonOffsetFactor;
				currentState.polygonOffsetUnits = polygonOffsetUnits;
			}
		}
	}

	// sets values to the current OpenGL state
	void getCurrentState() {
		int[] iBuf = new int[1];
		float[] fBuf = new float[1];

		getGpuState().getGl().glGetFloatv(GL.GL_POINT_SIZE, fBuf, 0);
		pointSize = fBuf[0];

		getGpuState().getGl().glGetFloatv(GL.GL_LINE_WIDTH, fBuf, 0);
		lineWidth = fBuf[0];

		// BUG: this doesn't work
		// spec says the initial value is GL_FILL though, so we just set that
		//int[] polygonModeResult = new int[2];
		//getGpuState().getGl().glGetIntegerv(GL3.GL_POLYGON_MODE, polygonModeResult, 0);
		//polygonModeFront = polygonModeResult[0];
		//polygonModeBack = polygonModeResult[1];
		polygonModeFront = GL3.GL_FILL;
		polygonModeBack = GL3.GL_FILL;

		cullingEnabled = getGpuState().getGl().glIsEnabled(GL.GL_CULL_FACE);

		getGpuState().getGl().glGetIntegerv(GL.GL_CULL_FACE_MODE, iBuf, 0);
		cullMode = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_FRONT_FACE, iBuf, 0);
		frontFaceOrientation = iBuf[0];

		polygonOffsetFillEnabled = getGpuState().getGl().glIsEnabled(GL.GL_POLYGON_OFFSET_FILL);
		polygonOffsetLineEnabled = getGpuState().getGl().glIsEnabled(GL3.GL_POLYGON_OFFSET_LINE);
		polygonOffsetPointEnabled = getGpuState().getGl().glIsEnabled(GL3.GL_POLYGON_OFFSET_POINT);

		getGpuState().getGl().glGetFloatv(GL.GL_POLYGON_OFFSET_FACTOR, fBuf, 0);
		polygonOffsetFactor = fBuf[0];

		getGpuState().getGl().glGetFloatv(GL.GL_POLYGON_OFFSET_UNITS, fBuf, 0);
		polygonOffsetUnits = fBuf[0];
	}
}